-------------Space Journey-------------
A 4am crack                  2020-06-28
-------------------. updated 2021-09-06
                   |___________________

Name: Space Journey
Genre: educational
Year: 1984
Credits: Roklan Corporation
Publisher: Scott, Foresman and Company
Platform: Apple ][ (48K)
Media: 5.25-inch disk
Sides: 1
OS: custom
Previous cracks: none

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  immediate disk read error

Locksmith Fast Disk Backup
  unable to read any track

EDD 4 bit copy (no sync, no count)
  errors on tracks $08-$22
  copy reboots endlessly

Copy ][+ nibble editor
  Tracks $08-$22 are unformatted
  Lower tracks look like a standard
  16-sector disk with modified
  address and data epilogues
  ($FF $FF $EB), but on closer
  inspection, the address fields are
  corrupted in a very specific way...

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK: 01  START: 21FD  LENGTH: 1860

21D8: FF FF FF FF FF FF FF FF   VIEW
21E0: FF FF FF FF FF FF FF FF
21E8: FF FF FF FF FF FF FF FF
21F0: FF FF FF FF FF FF FF FF
21F8: FF FF FF FF FF D5 AA 96  <-21FD
2200: AA AA AA AA AA AA AA AA
      ^^^^^ ^^^^^ ^^^^^ ^^^^^
      V=000 T=$00 S=$00 chksm

2208: FF FF EB BF F3 FC FF FF
2210: FF FF FF FF D5 AA AD 96
2218: 96 96 96 96 96 96 96 96

                 --^--

  This is track 1, but the address
  field claims it's track 0. The disk
  is lying to us. Bad disk, no biscuit!

Why didn't COPYA work?
  modified epilogues, plus corrupted
  address fields

Why didn't Locksmith FDB work?
  ditto

Why didn't EDD work?
  I don't know, but I do know that
  disks do not spontaneously reboot
  unless someone tells them to.

Once the game is loaded, it never uses
the disk again. There is are no user
records and no reload when a game ends.
I think this will be one of those
"capture the game in memory and rebuild
it from the ground up" cracks, with a
side quest to see why the EDD bit copy
reboots.

Next steps:

  1. Trace bootloader
  2. Capture game code in memory
  3. Write game to a standard disk and
     build a bootloader to load it
  4. Declare victory (*)

(*) but do not go to the gym until
    there's a vaccine

                   ~

               Chapter 1
          In Which We Return
         From Whence We Came,
        Which Is A Weird Thing
          To Do In Chapter 1


[S6,D1=original disk]
[S5,D1=my work disk]

]PR#5
...
]CALL -151

*9600<C600.C6FFM

; instead of jumping to on-disk code,
; copy boot sector to higher memory so
; it survives a reboot
96F8-   A0 00       LDY   #$00
96FA-   B9 00 08    LDA   $0800,Y
96FD-   99 00 28    STA   $2800,Y
9700-   C8          INY
9701-   D0 F7       BNE   $96FA

; turn off slot 6 drive motor
9703-   AD E8 C0    LDA   $C0E8

; reboot to my work disk in slot 5
9706-   4C 00 C5    JMP   $C500

*BSAVE TRACE0,A$9600,L$109
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0800-08FF,A$2800,L$100
]CALL -151

*800<2800.28FFM

*801L

; set up vector based on boot slot
0801-   8A          TXA
0802-   4A          LSR
0803-   4A          LSR
0804-   4A          LSR
0805-   4A          LSR
0806-   09 C0       ORA   #$C0
0808-   85 3F       STA   $3F

; set reset vector
080A-   8D F3 03    STA   $03F3
080D-   49 A5       EOR   #$A5
080F-   8D F4 03    STA   $03F4
0812-   A9 00       LDA   #$00
0814-   8D F2 03    STA   $03F2

; push a single byte to the stack
; (suspicious)
0817-   A9 6F       LDA   #$6F
0819-   48          PHA

; TEXT / PR#0 / IN#0 (normal machine
; initialization)
081A-   8D 81 C0    STA   $C081
081D-   20 2F FB    JSR   $FB2F
0820-   8D 52 C0    STA   $C052
0823-   20 89 FE    JSR   $FE89
0826-   20 93 FE    JSR   $FE93

; clear and display hi-res page 1
0829-   A2 20       LDX   #$20
082B-   A0 00       LDY   #$00
082D-   84 06       STY   $06
082F-   A9 20       LDA   #$20
0831-   85 07       STA   $07
0833-   98          TYA
0834-   91 06       STA   ($06),Y
0836-   C8          INY
0837-   D0 FB       BNE   $0834
0839-   E6 07       INC   $07
083B-   CA          DEX
083C-   D0 F6       BNE   $0834
083E-   8D 57 C0    STA   $C057
0841-   8D 50 C0    STA   $C050
0844-   8D 54 C0    STA   $C054
0847-   8D 52 C0    STA   $C052

; finish setting up vector in ($3E)
; which now points to $Cn5C (n=boot
; slot) for reading additional sectors
; via the drive firmware
084A-   A9 5C       LDA   #$5C
084C-   85 3E       STA   $3E
084E-   A9 60       LDA   #$60
0850-   20 F0 08    JSR   $08F0

*8F0L

; it seems like this was patched in,
; late in the development process?
; otherwise why call a subroutine
; just to store one value (it's an
; RTS, BTW, in $0801 so now we can
; JSR ($3E) to read a sector and it
; will return gracefully)
08F0-   8D 01 08    STA   $0801

; and also clear the text screen
08F3-   4C 58 FC    JMP   $FC58

Continuing from $0853...

; push a second byte to the stack --
; aha! now we have pushed a return
; address ($6FFF + 1 = $7000)
0853-   A9 FF       LDA   #$FF
0855-   48          PHA

; set up a counter of some kind
0856-   A0 00       LDY   #$00
0858-   84 FC       STY   $FC
085A-   C8          INY

; $27 is the zero page address used by
; the drive controller firmware for the
; address to store a sector it reads
085B-   A9 70       LDA   #$70
085D-   A2 04       LDX   #$04
085F-   85 27       STA   $27
0861-   E8          INX
0862-   86 49       STX   $49
0864-   84 F9       STY   $F9

; $3D is the address of the physical
; sector ID to read
0866-   B9 7D 08    LDA   $087D,Y
0869-   85 3D       STA   $3D
086B-   20 78 08    JSR   $0878

*878L

; X = boot slot x 16 (required for the
; drive controller firmware to hit the
; correct softswitches)
0878-   A6 2B       LDX   $2B

; this is the only way to JSR ($3E) in
; 6502 assembly language -- JSR to a
; function that JMPs to the indirect
; address
087A-   6C 3E 00    JMP   ($003E)

Oh to have just a few more opcodes! But
we made it work, didn't we?

Continuing from $086E...

; increment the counter we initialized
; earlier and branch back until we're
; done
086E-   A4 F9       LDY   $F9
0870-   C8          INY
0871-   C4 49       CPY   $49
0873-   90 EF       BCC   $0864

So this is a loop that reads additional
sectors from track 0 into $7000+. $F9
was initialized at #$01 and $49 at #$4,
so we're reading four sectors into
$7000..$73FF.

0875-   A5 27       LDA   $27

; "return" from whence we came
0877-   60          RTS

Return to... where? The two separate
PHAs mean that execution now continues
at $7000, which BY AN AMAZING(*)
COINCIDENCE is where we just stored the
new sectors we read from track 0.

(*) not guaranteed, actual amazement
    may vary

And that is how I will interrupt the
boot: by changing the bytes that we
push to the stack, so when we "return"
we actually end up in code I control.

                   ~

               Chapter 2
            The Whole Boot


*9600<C600.C6FFM

; change the two bytes that we push to
; the stack, so they point to a routine
; under my control ($9705 in this case)
96F8-   A9 97       LDA   #$97
96FA-   8D 18 08    STA   $0818
96FD-   A9 04       LDA   #$04
96FF-   8D 54 08    STA   $0854

; start the boot
9702-   4C 01 08    JMP   $0801

; execution will continue here from the
; "RTS" at $0877 --
; turn off the slot 6 drive motor and
; reboot to my work disk so I can save
; the code that was loaded at $7000
9705-   AD E8 C0    LDA   $C0E8
9708-   4C 00 C5    JMP   $C500

*BSAVE TRACE1,A$9600,L$10B

*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.7000-73FF,A$7000,L$400
]CALL -151

*7000L

7000-   4C 44 73    JMP   $7344

*7344L

; not shown, but this subroutine swaps
; 4 pages of memory from the two
; starting addresses given in A and Y
; (so $0400-$07FF gets swapped with
; $6C00-$6FFF)
7344-   A9 04       LDA   #$04
7346-   A0 6C       LDY   #$6C
7348-   20 E0 73    JSR   $73E0

; set up some addresses based on the
; boot slot
734B-   A6 2B       LDX   $2B
734D-   8A          TXA
734E-   09 8C       ORA   #$8C
7350-   8D 61 70    STA   $7061
7353-   8D 78 70    STA   $7078
7356-   8D 8E 70    STA   $708E
7359-   8D A2 70    STA   $70A2
735C-   8D B7 70    STA   $70B7

; index of some kind
735F-   A9 00       LDA   #$00
7361-   85 FB       STA   $FB
7363-   8D FF 02    STA   $02FF

; taking the index (at $FB), we copy a
; few bytes into zero page
7366-   A0 00       LDY   #$00
7368-   A6 FB       LDX   $FB
736A-   BD 21 73    LDA   $7321,X
736D-   99 3D 00    STA   $003D,Y
7370-   E8          INX
7371-   C8          INY
7372-   C0 05       CPY   #$05
7374-   90 F4       BCC   $736A

; and store the updated index back
7376-   86 FB       STX   $FB

This is looking like a loop that takes
a number of parameters, initially at
$7321, but copied into zero page for
the duration of one iteration through
the loop. Storing the index back into
$FB suggests that we'll operate on a
number of sets of parameters before
we're done. It's the assembly language
equivalent of calling a function with
five arguments. The rest of the code
doesn't need to keep track of the index
into $7321 because it just operates on
whatever is in zero page $3D..$41.

; if parameter #1 is 0, branch forward
7378-   A5 3D       LDA   $3D
737A-   F0 33       BEQ   $73AF

; on the other hand, if it's negative,
; branch forward but somewhere else
737C-   30 54       BMI   $73D2

*73D2L

73D2-   A5 3E       LDA   $3E
73D4-   85 3A       STA   $3A
73D6-   A5 3F       LDA   $3F
73D8-   85 3B       STA   $3B
73DA-   20 CF 73    JSR   $73CF
73DD-   4C 66 73    JMP   $7366

*73CFL

73CF-   6C 3A 00    JMP   ($003A)

Ah! If parameter #1 is negative, we end
up at $73D2, which copies a 2-byte
address from parameters #2 and #3 into
$3A/$3B and calls it like a subroutine,
again using the technique of the JSR
that calls an indirect JMP. Then we
unconditionally JMP back to $7366 to
operate on the next set of parameters.
Neat.

Continuing from $737E...

; if parameter #1 (in $3D) is positive
; and non-zero, we fall through
737E-   A6 2B       LDX   $2B

; not shown, but $70DA seeks to the
; phase (track x 2) given in A, which
; means that $40 (parameter #4) is a
; track number
7380-   A5 40       LDA   $40
7382-   0A          ASL
7383-   20 DA 70    JSR   $70DA

7386-   20 37 71    JSR   $7137

*7137L

; absolutely bog standard code to match
; the $D5 $AA $96 address prologue
7137-   A0 FC       LDY   #$FC
7139-   84 26       STY   $26
713B-   C8          INY
713C-   D0 04       BNE   $7142
713E-   E6 26       INC   $26
7140-   F0 51       BEQ   $7193
7142-   BD 8C C0    LDA   $C08C,X
7145-   10 FB       BPL   $7142
7147-   C9 D5       CMP   #$D5
7149-   D0 F0       BNE   $713B
714B-   EA          NOP
714C-   BD 8C C0    LDA   $C08C,X
714F-   10 FB       BPL   $714C
7151-   C9 AA       CMP   #$AA
7153-   D0 F2       BNE   $7147
7155-   A0 03       LDY   #$03
7157-   BD 8C C0    LDA   $C08C,X
715A-   10 FB       BPL   $7157
715C-   C9 96       CMP   #$96
715E-   D0 E7       BNE   $7147

; absolutely bog standard code to
; parse a 4-and-4-encoded address field
7160-   A9 00       LDA   #$00
7162-   85 27       STA   $27
7164-   BD 8C C0    LDA   $C08C,X
7167-   10 FB       BPL   $7164
7169-   2A          ROL
716A-   85 26       STA   $26
716C-   BD 8C C0    LDA   $C08C,X
716F-   10 FB       BPL   $716C
7171-   25 26       AND   $26
7173-   99 2C 00    STA   $002C,Y
7176-   45 27       EOR   $27
7178-   88          DEY
7179-   10 E7       BPL   $7162
717B-   A8          TAY
717C-   D0 15       BNE   $7193

; match custom address epilogue
; ($FF $FF instead of $DE $AA)
717E-   BD 8C C0    LDA   $C08C,X
7181-   10 FB       BPL   $717E
7183-   C9 FF       CMP   #$FF
7185-   D0 0C       BNE   $7193
7187-   EA          NOP
7188-   BD 8C C0    LDA   $C08C,X
718B-   10 FB       BPL   $7188
718D-   C9 FF       CMP   #$FF
718F-   D0 02       BNE   $7193

; C clear for success
7191-   18          CLC
7192-   60          RTS

; C set for failure
7193-   38          SEC
7194-   60          RTS

Continuing from $7389...

; loop forever to find an address field
; that parses correctly, because what
; even is error handling
7389-   B0 FB       BCS   $7386

; $41 is parameter #5, which we are
; comparing to the (physical) sector ID
; we just found
738B-   A5 41       LDA   $41
738D-   C5 2D       CMP   $2D

; branch back if that's not the sector
; we wanted
738F-   D0 F5       BNE   $7386

; not shown, but $7014 matches the
; standard data prologue ($D5 $AA $AD),
; parses a standard 6-and-2-encoded
; data field, stores the decoded data
; in ($3E) (which is parameters #2 and
; #3), then finally matches a custom
; data epilogue ($FF instead of $DE)
7391-   20 14 70    JSR   $7014

; if that didn't work, loop back to
; find another sector
7394-   B0 F0       BCS   $7386

; $3D (parameter #1) operates as a
; counter for how many sectors we want
; to read
7396-   C6 3D       DEC   $3D

; branch forward if there are more
; sectors to read
7398-   D0 03       BNE   $739D

; otherwise branch way back to the
; beginning of the loop, to copy and
; operate on the next set of parameters
739A-   4C 66 73    JMP   $7366

; $3F (parameter #3) is the high byte of
; the address we're using to store the
; sector data, so it makes sense that
; we're auto-incrementing it here
739D-   E6 3F       INC   $3F

; also auto-increment the sector number
; (parameter #5)
739F-   E6 41       INC   $41

; at sector #$10, wrap back to 0 and
; increment the track number (parameter
; #4), then branch back to seek to that
; new track
73A1-   A5 41       LDA   $41
73A3-   C9 10       CMP   #$10
73A5-   90 DF       BCC   $7386
73A7-   E6 40       INC   $40
73A9-   A9 00       LDA   #$00
73AB-   85 41       STA   $41
73AD-   F0 D1       BEQ   $7380

; execution continues here (from the
; BEQ at $737A) if parameter #1 was 0,
; and it appears that that was indeed
; the exit condition because now we're
; turning off the drive motor and
; moving on to other things
73AF-   A6 2B       LDX   $2B
73B1-   BD 88 C0    LDA   $C088,X

; move parameters #2 and #3 into place
; for an indirect JMP
73B4-   A5 3E       LDA   $3E
73B6-   85 3A       STA   $3A
73B8-   A5 3F       LDA   $3F
73BA-   85 3B       STA   $3B

; swap back the 4 pages we saved at
; $6C00 (back to $0400, the text page)
73BC-   A9 6C       LDA   #$6C
73BE-   A0 04       LDY   #$04
73C0-   20 E0 73    JSR   $73E0

; show the text page
73C3-   2C 51 C0    BIT   $C051

; copy #$20 pages from $8000+ to $2000+
73C6-   A9 80       LDA   #$80
73C8-   A0 20       LDY   #$20
73CA-   A2 20       LDX   #$20
73CC-   20 E2 73    JSR   $73E2

; jump to the game entry point, which
; was given in parameters #2 and #3 of
; the final parameter block
73CF-   6C 3A 00    JMP   ($003A)

This is just an amazingly elegant loop.
I love everything about it. Here, then,
are the blocks of parameters on which
it will operate:

*7321.7325

7321- 08 00 08 01 00

"Read 8 sectors into $0800, starting
 at track 1, sector 0."

*7326.732A

7326- 02 00 05 01 08

"Read 2 sectors into $0500 starting
 at track 1, sector 8."

*732B.732F

732B- FF 00 05 00 00

"Call $0500."

*7330.7334

7330- 10 00 10 02 00

"Read $10 sectors into $1000, starting
 at track 2, sector 0."

*7335.7339

7335- 20 00 80 03 00

"Read $20 sectors into $8000, starting
 at track 3, sector 0."

*733A.733E

733A- 2B 00 40 05 00

"Read $2B sectors into $4000, starting
 at track 5, sector 0."

*733F.7343

733F- 00 00 08 00 00

"Call $0800."

That's it; that's the whole boot. $0800
is the game entry point. Before calling
it (at $73CF), we manually copy $8000..
$9FFF into $2000..$3FFF (at $73C6) to
avoid seeing any code on the hi-res
page during the boot.

One optimization they could have made
is leaving the text page blank, loading
the code at $0500 elsewhere, then
loading directly into $2000. But I
assume they had their reasons.

                   ~

               Chapter 3
  In Which We Discover Their Reasons


I've changed my mind. I don't want to
capture this game in memory and rewrite
the bootloader. The bootloader is fine.
The disk is fine. The only problem is
that the address field of every sector
claims that it's on track 0, even when
it isn't.

Did you notice that? I bet you didn't.
I didn't for a while. We saw in the
Copy ][+ nibble editor that the address
field of sectors on track 1 claim to be
track 0. We saw at $7137 that we parse
the address field into zero page
$2C..$2F, just like a normal DOS 3.3
disk. And we saw at $717B that we
verify the address field checksum.

But then... the caller never checks the
track number. It just ignores it. At
$738D, it compares the sector number in
$2D, but it does not care one whit
about the track number.

This bootloader is very close to being
able to read a standard disk. If I
changed the address and data epilogues
from $FF $FF to $DE $AA, the rest of
this disk reading code would work
unmodified. "Ignore the track number"
works just as well if the track number
happens to be correct.

How do I get the data off this disk and
onto a standard disk that is arranged
exactly the same but uses uncorrupted
address fields? It seems like I should
be able to read this disk with the
standard DOS 3.3 RWTS, if I could patch
it to match the custom epilogues (can
do), then further patch it to ignore
track numbers (no idea). Then I could
use Advanced Demuffin to read the
original disk and copy the data to a
standard disk.

Patching a DOS 3.3 RWTS to match custom
epilogues is no problem. But getting it
to ignore track numbers is something I
have never attempted.

Here we go.

[S6,D1=standard DOS 3.3 disk]

]PR#6
...

]CALL -151

*B98BL

; match address epilogue
B98B-   BD 8C C0    LDA   $C08C,X
B98E-   10 FB       BPL   $B98B
B990-   C9 DE       CMP   #$DE      <--
B992-   D0 AE       BNE   $B942
B994-   EA          NOP
B995-   BD 8C C0    LDA   $C08C,X
B998-   10 FB       BPL   $B995
B99A-   C9 AA       CMP   #$AA      <--
B99C-   D0 A4       BNE   $B942
B99E-   18          CLC
B99F-   60          RTS

I can change those $DE and $AA bytes
to $FF, to match this disk's custom
address epilogue.

*B92FL

; match data epilogue
B92F-   BD 8C C0    LDA   $C08C,X
B932-   10 FB       BPL   $B92F
B934-   C9 DE       CMP   #$DE      <--
B936-   D0 0A       BNE   $B942
B938-   EA          NOP
B939-   BD 8C C0    LDA   $C08C,X
B93C-   10 FB       BPL   $B939
B93E-   C9 AA       CMP   #$AA      <--
B940-   F0 5C       BEQ   $B99E
B942-   38          SEC
B943-   60          RTS

Similarly, the bytes at $B935 and $B93F
can become $FF to match the disk's data
epilogue.

Now the hard part. The relevant code
starts at $BDC1.

*BDC1L

; X = index based on slot
BDC1-   AE F8 05    LDX   $05F8

; find and parse address field into
; zero page $2C..$2F
BDC4-   20 44 B9    JSR   $B944

; if that worked (we found an address
; field AND its internal checksum was
; verified AND we found the epilogue),
; branch ahead
BDC7-   90 24       BCC   $BDED

...

; Y = track number from address field
BDED-   A4 2E       LDY   $2E

; compare to the expected track number
BDEF-   CC 78 04    CPY   $0478

; if they match, branch ahead
BDF2-   F0 1C       BEQ   $BE10

; otherwise we're on the wrong track,
; so do that grinding thing where we
; recalibrate to track 0 and back
BDF4-   AD 78 04    LDA   $0478
BDF7-   48          PHA
BDF8-   98          TYA
BDF9-   20 95 BE    JSR   $BE95
BDFC-   68          PLA
BDFD-   CE F8 04    DEC   $04F8
BE00-   D0 E5       BNE   $BDE7
BE02-   F0 CA       BEQ   $BDCE

That's it. The next check is the disk
volume number, then the sector number,
then the rest is all happy-path code.
It turns out there's only one check of
the found track, it's immediately after
finding and parsing the address field,
and we can skip over it entirely by
changing one branch:

; read address field as usual
BDC1-   AE F8 05    LDX   $05F8
BDC4-   20 44 B9    JSR   $B944

; but branch over the track check
BDC7-   90 47       BCC   $BE10     <--

Thus:

*B991:FF
*B99B:FF
*B935:FF
*B93F:FF
*BDC8:47

*3800<B800.BFFFM

*C500G
...

]BSAVE RWTS,A$3800,L$800

And now we have a DOS-shaped RWTS that
can read the original disk.

]BRUN ADVANCED DEMUFFIN 1.5

["5" to switch to slot 5]

["R" to load a new RWTS module]
  --> At $B8, load "RWTS" from drive 1

["6" to switch to slot 6]

["C" to convert disk]

["Y" to change default values]

                 --v--

ADVANCED DEMUFFIN 1.5    (C) 1983, 2014
ORIGINAL BY THE STACK    UPDATES BY 4AM
=======================================


INPUT ALL VALUES IN HEX


SECTORS PER TRACK? (13/16) 16

START TRACK: $00
START SECTOR: $00
END TRACK: $07          <-- change this
END SECTOR: $0F         <-- change this

INCREMENT: 1

MAX # OF RETRIES: 0

COPY FROM DRIVE 1
TO DRIVE: 2
=======================================
16SC $00,$00-$07,$0F BY1.0 S6,D1->S6,D2

                 --^--

[S6,D1=original disk]
[S6,D2=blank disk]

And here we go...

                 --v--

ADVANCED DEMUFFIN 1.5    (C) 1983, 2014
ORIGINAL BY THE STACK    UPDATES BY 4AM
=======PRESS ANY KEY TO CONTINUE=======
TRK:........
+.5:
    0123456789ABCDEF0123456789ABCDEF012
SC0:........
SC1:........
SC2:........
SC3:........
SC4:........
SC5:........
SC6:........
SC7:........
SC8:........
SC9:........
SCA:........
SCB:........
SCC:........
SCD:........
SCE:........
SCF:........
=======================================
16SC $00,$00-$07,$0F BY1.0 S6,D1->S6,D2

                 --^--

OMG it actually worked.

[S6,D1=demuffin'd disk]

]PR#6
...hangs of course, because it's still
   looking for the custom epilogues...

Turning to my trusty Disk Fixer sector
editor, I find the relevant epilogue
matching code on track 0.

T00,S05,$84: FF -> DE  ; address 1
T00,S05,$8E: FF -> AA  ; address 2
T00,S06,$CE: FF -> DE  ; data 1

]PR#6
...reboots endlessly...

OK, but that's progress. Explicitly
rebooting means the bootloader is
reading the disk successfully. There
was no error handling that would reboot
if it couldn't read a sector, so this
is something else. It's loading and
executing code that is choosing to
reboot because it doesn't like...
something. That's what my EDD bit copy
did, too.

Methinks it's time to look at the code
that gets loaded into the text page and
executed from $0500. Now that I've
fixed the corrupt address fields, I can
read all the tracks on my non-working
copy with a sector editor. So let's do
that.

One small speed bump: this bootloader
deals with physical sector numbers, not
the usual "logical" sector numbers that
DOS 3.3 uses. It's a bit confusing to
map them in my head. However, my trusty
Disk Fixer sector editor has a setting
for that. (I just learned this recently
and am very excited about it.)

In Disk Fixer, press "O" for the
"INPUT/OUTPUT CONTROL" page. There are
options for setting the expected
prologues and epilogues, but ignore
those. Further down is a part I've
literally never messed with, the sector
numbering:

                 --v--

 +--------- SECTOR NUMBERING ---------+
 !$00: $00 $0D $0B $09 $07 $05 $03 $01!
 !$08: $0E $0C $0A $08 $06 $04 $02 $0F!
 +------------- NIBBLES: -------------+

                 --^--

That's a mapping between physical and
logical sectors, so you can refer to a
sector by the number that DOS 3.3 would
call it, rather than the underlying
physical sector number stored in the
address field. But the entire section
is editable, so we can change it to say
that logical sectors map directly to
physical sectors:

                 --v--

 +--------- SECTOR NUMBERING ---------+
 !$00: $00 $01 $02 $03 $04 $05 $06 $07!
 !$08: $08 $09 $0A $0B $0C $0D $0E $0F!
 +------------- NIBBLES: -------------+

                 --^--

Now we can give Disk Fixer the same
sector numbers that the bootloader
uses, and it will read the disk the
same way.

The bootloader reads 8 sectors from
track 1, sector 0-7 into $0800+. That
looks like actual game code and data.
Then it reads 2 sectors from track 1,
sector 8-9 into $0500+ and calls it.
Since I've looked at all the rest of
the bootloader code, I suspect that
routine at $0500 is what's causing my
demuffin'd copy (and my failed EDD bit
copy) to reboot.

                 --v--

T01,S08 (physical)
----------- DISASSEMBLY MODE ----------
0000:4C 61 05       JMP   $0561

...
0061:20 00 06       JSR   $0600
0064:B0 01          BCS   $0067
0066:60             RTS
0067:A5 2B          LDA   $2B
0069:4A             LSR
006A:4A             LSR
006B:4A             LSR
006C:4A             LSR
006D:09 C0          ORA   #$C0
006F:A8             TAY
0070:88             DEY
0071:98             TYA
0072:48             PHA
0073:A9 FF          LDA   #$FF
0075:48             PHA
0076:4C 58 FC       JMP   $FC58

                 --^--

Aha! $0500 jumps to $0561, which calls
$0600 and -- if it returns with the
carry bit set -- pushes the address of
the reboot vector and exits via HOME
($FC58) to clear this code from memory.

This is why the bootloader went to so
much trouble to move code around rather
than just show the text page: they're
using the text page for something
sneaky.

So what's at $0600?

                   ~

               Chapter 4
  In Which We Run Into An Old Friend
 Although Frankly If Your Friends Are
 Constantly Preventing You From Doing
  What You Want To Do Then Maybe You
      Should Find Better Friends


Since we've mapped the sectors to match
the way the disk loads them, we can
find $0600 on the "next" sector:

                 --v--

T01,S09 (physical)
----------- DISASSEMBLY MODE ----------
; high byte of Death Counter
0000:A9 0A          LDA   #$0A
0002:85 50          STA   $50

; turn on drive motor
0004:A6 2B          LDX   $2B
0006:BD 89 C0       LDA   $C089,X
0009:BD 8E C0       LDA   $C08E,X

; probably an address, ($48) -> $067E
; which would make it part of this
; sector
000C:A9 7E          LDA   #$7E
000E:85 48          STA   $48
0010:A9 06          LDA   #$06
0012:85 49          STA   $49

; low byte of Death Counter
0014:A9 80          LDA   #$80
0016:85 51          STA   $51
0018:C6 51          DEC   $51

; when Death Counter hits 0, bad things
; happen (I'm assuming)
001A:F0 5C          BEQ   $0078

; this finds the next address prologue
; ($D5 $AA $96), parses the address
; field, verifies the address checksum,
; and matches the address epilogue
; (not shown, but it's essentially
; identical to the code at $7137, so I
; guess that's one more set of epilogue
; nibbles I get to patch)
001C:20 03 05       JSR   $0503

; if that didn't work, fail
001F:B0 57          BCS   $0078

; loop until we find sector $0F (in
; zero page $2D after routine at $0503)
0021:A5 2D          LDA   $2D
0023:C9 0F          CMP   #$0F
0025:D0 F1          BNE   $0018

; here we go
0027:A0 00          LDY   #$00
0029:BD 8C C0       LDA   $C08C,X
002C:10 FB          BPL   $0029
002E:88             DEY
002F:F0 47          BEQ   $0078  ; fail

; find $D5 nibble
0031:C9 D5          CMP   #$D5
0033:D0 F4          BNE   $0029

; find $E7 $E7 $E7 sequence of nibbles
; within the next $100 nibbles
; (Y register serves as the mini-Death
; Counter here, if it wraps around to 0
; then we give up)
0035:A0 00          LDY   #$00
0037:BD 8C C0       LDA   $C08C,X
003A:10 FB          BPL   $0037
003C:88             DEY
003D:F0 39          BEQ   $0078  ; fail
003F:C9 E7          CMP   #$E7
0041:D0 F4          BNE   $0037
0043:BD 8C C0       LDA   $C08C,X
0046:10 FB          BPL   $0043
0048:C9 E7          CMP   #$E7
004A:D0 2C          BNE   $0078  ; fail
004C:BD 8C C0       LDA   $C08C,X
004F:10 FB          BPL   $004C
0051:C9 E7          CMP   #$E7
0053:D0 23          BNE   $0078  ; fail

; reset data latch and kill some time
; to get out of sync with the original
; nibble boundary
0055:BD 8D C0       LDA   $C08D,X

; reset Y (serves as a mini-Death
; Counter again in the next loop)
0058:A0 10          LDY   #$10

; does nothing of consequence except
; burn a few more CPU cycles
005A:24 06          BIT   $06

; now start looking for nibbles that
; don't really exist (except they do,
; because we're out of sync and reading
; timing bits as data)
005C:BD 8C C0       LDA   $C08C,X
005F:10 FB          BPL   $005C
0061:88             DEY
0062:F0 14          BEQ   $0078  ; fail
0064:C9 EE          CMP   #$EE
0066:D0 F4          BNE   $005C

; check for (still desynchronized)
; nibble sequence stored in reverse
; order at ($48) a.k.a. $067E
0068:A0 07          LDY   #$07
006A:BD 8C C0       LDA   $C08C,X
006D:10 FB          BPL   $006A
006F:D1 48          CMP   ($48),Y
0071:D0 05          BNE   $0078  ; fail
0073:88             DEY
0074:10 F4          BPL   $006A
0076:18             CLC          ; pass
0077:60             RTS

; failure path ends up here, from
; multiple places noted above --
; decrement the outer Death Counter and
; restart the entire process
0078:C6 50          DEC   $50
007A:D0 98          BNE   $0014

; give up, return to caller with C set
007C:38             SEC
007D:60             RTS

; array nibbles to look for after we've
; intentionally desynchronized (by
; burning cycles at $0655 and $0658)
007E:FC EE EE FC E7 EE FC E7

                 --^--

This protection check is failing before
it even gets started, because it can't
find the custom address epilogue.

T01,S08,$50: $FF -> $DE
T01,S08,$5A: $FF -> $AA

]PR#6
...still reboots endlessly, but now
   because it can't find the
   desynchronized nibble stream...

We can bypass this entire protection
check by reproducing the success path
at $0676 -- clearing the carry and
returning to the caller.

T01,S08,$00: $A9 $0A -> $18 $60

]PR#6
...works, and it is glorious...

Quod erat liberandum.

                   ~

               Changelog

2021-09-06

- Patch protection code instead of
  using no-code E7 patch.
- Removed previous write-up of E7,
  which was wrong in several ways. See
  4am crack no. 655 Rocky's Boots v4.0
  for a full explanation of the E7
  protection.

2020-06-29

- typos and some reformatting

2020-06-28

- initial release

---------------------------------------
A 4am crack                    No. 2205
------------------EOF------------------
